﻿namespace CookieTerm
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Drawing;
    using System.Linq;
    using System.Text;
    using System.Windows.Forms;

    public partial class BbsConsole : Control, IWindow
    {
        /// <summary>
        /// Mappings from platform specific (Windows forms, in this case) keys to platform independent keys.
        /// </summary>
        static Dictionary<Keys, Key> keyMappings = new Dictionary<Keys, Key>
        {
            {Keys.Up, Key.Up},
            {Keys.Down, Key.Down},
            {Keys.Right, Key.Right},
            {Keys.Left, Key.Left},
            {Keys.Insert, Key.Insert},
            {Keys.Delete, Key.Delete},
            {Keys.Home, Key.Home},
            {Keys.End, Key.End},
            {Keys.Prior, Key.Prior},
            {Keys.Next, Key.Next},
            {Keys.Tab, Key.Tab}
        };

        Bookmark bookmark;
        Controller controller;

        EventHandler<ConsoleInputReceivedEventArgs> onConsoleInputReceived;
        string statusText = string.Empty;
        Timer blinkingTimer;
        Timer antiIdleTimer;
        bool blinkingVisible = true;

        #region Native drawing resources.
        Font normalFont;
        Font underlineFont;
        Brush[] brushes;
        Brush[] boldBrushes;
        Brush cursorBrush;

        int charWidth;
        int charHeight;
        #endregion

        #region Text selection related variables
        bool mouseDragging;
        Nullable<Point> draggingStart;

        bool hasSelection;
        ConsoleCell selectionStart;
        ConsoleCell selectionEnd;
        #endregion

        public event EventHandler OnStatusChanged;

        public Controller Controller
        {
            get
            {
                return this.controller;
            }
        }

        Font NormalFont
        {
            get
            {
                if (this.normalFont == null)
                {
                    this.normalFont = new Font(this.bookmark.DisplaySettings.FontName, this.bookmark.DisplaySettings.FontSize);
                }

                return this.normalFont;
            }
        }

        Font UnderlineFont
        {
            get
            {
                if (this.underlineFont == null)
                {
                    this.underlineFont = new Font(this.NormalFont, FontStyle.Underline);
                }

                return this.underlineFont;
            }
        }

        Brush[] Brushes
        {
            get
            {
                if (this.brushes == null)
                {
                    this.brushes = this.bookmark.DisplaySettings.Palette.Select(color => new SolidBrush(color.ToColor())).ToArray();
                }

                return this.brushes;
            }
        }

        Brush[] BoldBrushes
        {
            get
            {
                if (this.boldBrushes == null)
                {
                    this.boldBrushes = this.bookmark.DisplaySettings.BoldPalette.Select(color => new SolidBrush(color.ToColor())).ToArray();
                }

                return this.boldBrushes;
            }
        }

        Brush CursorBrush
        {
            get
            {
                if (this.cursorBrush == null)
                {
                    this.cursorBrush = new SolidBrush(this.bookmark.DisplaySettings.CursorColor.ToColor());
                }

                return this.cursorBrush;
            }
        }

        int Rows
        {
            get
            {
                return this.bookmark.DisplaySettings.Rows;
            }
        }

        int Columns
        {
            get
            {
                return this.bookmark.DisplaySettings.Columns;
            }
        }

        public BbsConsole(Bookmark bookmark)
        {
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            Cursor = Cursors.IBeam;

            #region Set up context menu
            ContextMenuStrip = new ContextMenuStrip();
            ToolStripMenuItem copyMenuItem = new ToolStripMenuItem();
            ToolStripMenuItem pasteMenuItem = new ToolStripMenuItem();
            ToolStripMenuItem selectAllMenuItem = new ToolStripMenuItem();
            ToolStripMenuItem launchAsUrlMenuItem = new ToolStripMenuItem();
            ContextMenuStrip.SuspendLayout();

            // ContextMenu
            ContextMenuStrip.Items.Add(copyMenuItem);
            ContextMenuStrip.Items.Add(pasteMenuItem);
            ContextMenuStrip.Items.Add(selectAllMenuItem);
            ContextMenuStrip.Items.Add(launchAsUrlMenuItem);
            ContextMenuStrip.Name = "editContextMenu";
            ContextMenuStrip.Size = new Size(153, 114);

            // copyMenuItem
            copyMenuItem.Name = "copyMenuItem";
            copyMenuItem.Size = new Size(152, 22);
            copyMenuItem.Text = "&Copy";
            copyMenuItem.Click += new EventHandler(this.copyMenuItem_Click);

            // pasteMenuItem
            pasteMenuItem.Name = "pasteMenuItem";
            pasteMenuItem.Size = new Size(152, 22);
            pasteMenuItem.Text = "&Paste";
            pasteMenuItem.Click += new EventHandler(this.pasteMenuItem_Click);

            // selectAllMenuItem
            selectAllMenuItem.Name = "selectAllMenuItem";
            selectAllMenuItem.Size = new System.Drawing.Size(152, 22);
            selectAllMenuItem.Text = "Select &All";
            selectAllMenuItem.Click += new System.EventHandler(this.selectAllMenuItem_Click);

            // launchAsURLMenuItem
            launchAsUrlMenuItem.Name = "launchAsUrlMenuItem";
            launchAsUrlMenuItem.Size = new System.Drawing.Size(152, 22);
            launchAsUrlMenuItem.Text = "Launch &URL";
            launchAsUrlMenuItem.Click += new System.EventHandler(this.launchAsURLMenuItem_Click);
            ContextMenuStrip.ResumeLayout(false);
            #endregion

            this.bookmark = bookmark;

            // Set up timer for cursor and character blinking
            this.blinkingTimer = new Timer { Interval = this.bookmark.DisplaySettings.BlinkingInterval };
            this.blinkingTimer.Tick += OnBlinkingTimer_Tick;

            // Set up timer for anti idle
            if (this.bookmark.DisplaySettings.AntiIdle)
            {
                this.antiIdleTimer = new Timer { Interval = CookieTermAppSettings.AntiIdleInterval * 1000 };
                this.antiIdleTimer.Tick += OnAntiIdleTimerElapsed;
            }

            // Create a controller
            this.controller = new Controller(bookmark, this);
        }

        /// <summary>
        /// Refresh the look of the console, based on new display settings. Note that the rows anc columns 
        /// of the console will not be updated. They are fixed when the console is constructed.
        /// </summary>
        public void RefreshDisplaySettings()
        {
            // Discard drawing resources. They will be allocated when accessed for the first time.
            this.brushes = null;
            this.boldBrushes = null;
            this.cursorBrush = null;
            this.normalFont = null;
            this.underlineFont = null;

            // Calculate console size in pixels
            Graphics g = CreateGraphics();

            // Let's assume W and g are the widest and tallest characters. It may not be an accurate assumption though.
            SizeF size = g.MeasureString("W", NormalFont, new PointF(0, 0), StringFormat.GenericTypographic);
            this.charWidth = (int)size.Width;

            size = g.MeasureString("g", NormalFont, new PointF(0, 0), StringFormat.GenericTypographic);
            this.charHeight = (int)size.Height;

            int w = this.charWidth * Columns;
            int h = this.charHeight * Rows;
            Size = new Size(w, h);

            BackColor = this.bookmark.DisplaySettings.Black.ToColor();

            Invalidate();
        }

        public void SelectAll()
        {
            this.StartTextSelection(1, 1);
            this.ExtendTextSelectionTo(Rows, Columns);
        }

        public void Copy()
        {
            string selection = this.GetSelectedText(true);
            if (!string.IsNullOrEmpty(selection))
            {
                Clipboard.SetText(selection);
            }
        }

        public void Paste()
        {
            string s = Clipboard.GetText();
            if (!string.IsNullOrEmpty(s) && this.OnConsoleInputReceived != null)
            {
                byte[] input = this.bookmark.DisplaySettings.Encoding.GetBytes(s);
                this.OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(input));
            }
        }

        public void LaunchSelectionAsUrl()
        {
            string s = this.GetSelectedText(true);
            if (!string.IsNullOrEmpty(s))
            {
                if (s.StartsWith("http://") || s.StartsWith("https://"))
                {
                    Process.Start(s);
                }
            }
        }

        public void SimulateKeyPress(char c)
        {
            this.OnKeyPress(new KeyPressEventArgs(c));
        }

        protected override void OnCreateControl()
        {
            base.OnCreateControl();

            this.RefreshDisplaySettings();

            if (this.blinkingTimer != null)
            {
                this.blinkingTimer.Start();
            }

            this.controller.BeginConnect();
        }

        protected override void OnKeyPress(KeyPressEventArgs e)
        {
            byte[] input;

            char c = e.KeyChar;

            if (Controller.isAscii(c))
            {
                input = new byte[] { (byte)c };
            }
            else
            {
                input = this.bookmark.DisplaySettings.Encoding.GetBytes(new char[] { c });
            }

            if (OnConsoleInputReceived != null)
            {
                OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(input));
            }
            e.Handled = true;
        }

        protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
        {
            // If the special key maps to an ascii sequence in the terminal settings, send
            // that sequence to the terminal.
            Key key;
            byte[] data;
            if (OnConsoleInputReceived != null
                && keyMappings.TryGetValue(keyData, out key)
                && this.bookmark.TerminalSettings.KeyToSequenceMapping.TryGetValue(key, out data))
            {
                OnConsoleInputReceived(this, new ConsoleInputReceivedEventArgs(data));
                return true;
            }
            return base.ProcessCmdKey(ref msg, keyData);
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                this.ClearTextSelection();
                this.draggingStart = new Point(e.X, e.Y);
            }
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            if (!this.mouseDragging && this.draggingStart.HasValue)
            {
                if (Math.Abs(e.X - this.draggingStart.Value.X) + Math.Abs(e.Y - this.draggingStart.Value.Y) > 4)
                {
                    this.mouseDragging = true;
                    this.StartTextSelection(ScreenCoordinateToConsoleCell(this.draggingStart.Value.X, this.draggingStart.Value.Y));
                }
            }

            if (this.mouseDragging)
            {
                ConsoleCell p = this.ScreenCoordinateToConsoleCell(e.X, e.Y);
                this.ExtendTextSelectionTo(p);
            }
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            this.mouseDragging = false;
            this.draggingStart = null;
        }

        protected override void OnClick(EventArgs e)
        {
            Focus();
            base.OnClick(e);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            for (int r = 1; r <= Rows; r++)
            {
                for (int c = 1; c <= Columns; c++)
                {
                    this.DrawCell(e.Graphics, r, c);
                }
            }

            base.OnPaint(e);
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                DisposeDrawingResources();
            }

            if (this.blinkingTimer != null)
            {
                this.blinkingTimer.Stop();
                this.blinkingTimer.Dispose();
                this.blinkingTimer = null;
            }
            this.blinkingTimer = null;

            if (this.antiIdleTimer != null)
            {
                this.antiIdleTimer.Stop();
                this.antiIdleTimer.Dispose();
                this.antiIdleTimer = null;
            }

            base.Dispose(disposing);
        }

        /// <summary>
        /// Draw a single cell on the console
        /// </summary>
        /// <param name="g">Graphics</param>
        /// <param name="row">Row of the cell</param>
        /// <param name="column">Column of the cell</param>
        void DrawCell(Graphics g, int row, int column)
        {
            CharAttribute cell = Controller.VideoBuffer[row, column];
            char ch = cell.Character;

            if (ch == 0)
            {
                return;
            }

            AnsiColor fgColor;
            AnsiColor bgColor;

            // Inverse attribute && selection
            if (cell.Attributes.HasInverse() ^ this.IsSelected(row, column))
            {
                fgColor = cell.Background;
                bgColor = cell.Foreground;
            }
            else
            {
                fgColor = cell.Foreground;
                bgColor = cell.Background;
            }

            // Underline attribute
            Font font = cell.Attributes.HasUnderline() ? this.UnderlineFont : this.NormalFont;

            // Non ASCII double width character
            bool doubleWidth = !Controller.isAscii(ch) && column < Columns;

            int x = (column - 1) * this.charWidth;
            int y = (row - 1) * this.charHeight;
            int w = doubleWidth ? this.charWidth * 2 : this.charWidth;
            int h = this.charHeight;

            // Cursor
            bool isCursor = this.blinkingVisible && Controller.ShowCursor && column == Controller.CursorColumn && row == Controller.CursorRow;

            Brush bgBrush = isCursor ? this.CursorBrush : this.Brushes[(int)bgColor];
            g.FillRectangle(bgBrush, x, y, w, h);

            if (!cell.Attributes.HasBlink() || this.blinkingVisible)
            {
                Brush fgBrush = (cell.Attributes.HasBold() ? this.BoldBrushes : this.Brushes)[(int)fgColor];
                g.DrawString(ch.ToString(), font, fgBrush, x, y, StringFormat.GenericTypographic);
            }
        }

        void OnBlinkingTimer_Tick(object sender, EventArgs e)
        {
            this.blinkingVisible = !this.blinkingVisible;
            Invalidate();
        }

        void OnAntiIdleTimerElapsed(object sender, EventArgs e)
        {
            SimulateKeyPress((char)CookieTermAppSettings.AntiIdleChar);
        }

        void ClearTextSelection()
        {
            if (this.hasSelection)
            {
                this.hasSelection = false;

                ConsoleCell min = ConsoleCell.min(this.selectionStart, this.selectionEnd);
                ConsoleCell max = ConsoleCell.max(this.selectionStart, this.selectionEnd);

                for (ConsoleCell c = min; c <= max; c++)
                {
                    this.InvalidateCell(c.row, c.column);
                }
            }
        }

        void StartTextSelection(int row, int column)
        {
            ConsoleCell p = new ConsoleCell(Columns, row, column);
            this.StartTextSelection(p);
        }

        void StartTextSelection(ConsoleCell p)
        {
            this.hasSelection = true;
            this.selectionStart = p;
            this.selectionEnd = p;

            this.InvalidateCell(p.row, p.column);
        }

        void ExtendTextSelectionTo(int row, int column)
        {
            ConsoleCell p = new ConsoleCell(Columns, row, column);
            this.ExtendTextSelectionTo(p);
        }

        void ExtendTextSelectionTo(ConsoleCell p)
        {
            if (p != this.selectionEnd)
            {
                ConsoleCell min = ConsoleCell.min(this.selectionEnd, p);
                ConsoleCell max = ConsoleCell.max(this.selectionEnd, p);

                this.selectionEnd = p;

                for (ConsoleCell c = min; c < max; c++)
                {
                    this.InvalidateCell(c.row, c.column);
                }
            }
        }

        string GetSelectedText(bool wrapLine)
        {
            if (this.hasSelection)
            {
                ConsoleCell min = ConsoleCell.min(this.selectionStart, this.selectionEnd);
                ConsoleCell max = ConsoleCell.max(this.selectionStart, this.selectionEnd);

                StringBuilder text = new StringBuilder();

                for (ConsoleCell c = min; c <= max; c++)
                {
                    char ch = Controller.VideoBuffer[c.row, c.column].Character;
                    if (ch != 0)
                    {
                        text.Append(Controller.VideoBuffer[c.row, c.column].Character);
                    }
                    if (wrapLine && c.column == Columns)
                    {
                        text.Append("\r\n");
                    }
                }

                return text.ToString();
            }
            else
            {
                return string.Empty;
            }
        }

        bool IsSelected(int row, int column)
        {
            if (this.hasSelection)
            {
                ConsoleCell p = new ConsoleCell(Columns, row, column);
                return (p >= this.selectionStart && p <= this.selectionEnd || p >= this.selectionEnd && p <= this.selectionStart);
            }
            else
            {
                return false;
            }
        }

        ConsoleCell ScreenCoordinateToConsoleCell(int x, int y)
        {
            int column = x / this.charWidth + 1;
            int row = y / this.charHeight + 1;

            if (row > Rows)
            {
                row = Rows;
            }

            if (column > Columns)
            {
                column = Columns;
            }

            if (row < 1)
            {
                row = 1;
            }

            if (column < 1)
            {
                column = 1;
            }

            if (column > 1 && Controller.VideoBuffer[row, column].Character == 0)
            {
                column--;
            }
            return new ConsoleCell(Columns, row, column);
        }

        void copyMenuItem_Click(object sender, EventArgs e)
        {
            this.Copy();
        }

        void launchAsURLMenuItem_Click(object sender, EventArgs e)
        {
            this.LaunchSelectionAsUrl();
        }

        void pasteMenuItem_Click(object sender, EventArgs e)
        {
            this.Paste();
        }

        void selectAllMenuItem_Click(object sender, EventArgs e)
        {
            this.SelectAll();
        }

        void DisposeDrawingResources()
        {
            if (this.normalFont != null)
            {
                this.normalFont.Dispose();
                this.normalFont = null;
            }

            if (this.underlineFont != null)
            {
                this.underlineFont.Dispose();
                this.underlineFont = null;
            }

            if (this.brushes != null)
            {
                foreach (Brush brush in this.brushes)
                {
                    brush.Dispose();
                }
            }
            this.brushes = null;

            if (this.boldBrushes != null)
            {
                foreach (Brush brush in this.boldBrushes)
                {
                    brush.Dispose();
                }
                this.boldBrushes = null;
            }

            if (this.cursorBrush != null)
            {
                this.cursorBrush.Dispose();
                this.cursorBrush = null;
            }
        }

        #region IWindow implementation
        public EventHandler<ConsoleInputReceivedEventArgs> OnConsoleInputReceived
        {
            get
            {
                return this.onConsoleInputReceived;
            }
            set
            {
                this.onConsoleInputReceived = value;
            }
        }

        public string StatusText
        {
            // We don't have a status bar. It's up to our container to display the status text.
            get
            {
                return this.statusText; 
            }
            set
            {
                this.statusText = this.bookmark.Name + " : " + value;
                if (OnStatusChanged != null)
                {
                    if (InvokeRequired)
                    {
                        Invoke(new Action(() => { OnStatusChanged(this, new EventArgs()); }));
                    }
                    else
                    {
                        OnStatusChanged(this, new EventArgs());
                    }
                }
            }
        }

        public void PromptForUserNamePassword(out string userName, out string password)
        {
            AuthenticationPrompt dialog = new AuthenticationPrompt();
            dialog.ShowDialog();
            userName = dialog.userNameTextBox.Text;
            password = dialog.passwordTextBox.Text;
        }

        public void ShowMessageBox(string message)
        {
            MessageBox.Show(message, CookieTermAppSettings.ApplicationName);
        }

        public bool ShowYesNoMessageBox(string message)
        {
            DialogResult response = MessageBox.Show(message, CookieTermAppSettings.ApplicationName, MessageBoxButtons.YesNo);
            return (response == DialogResult.Yes);
        }

        public void StartClosing()
        {
            CloseForm();
        }

        public void ResetAntiIdle()
        {
            if (this.antiIdleTimer != null)
            {
                // Since we are Windows form timer, it needs to be manipulated from the UI thread.
                if (InvokeRequired)
                {
                    Invoke(new Action(() =>
                    {
                        this.antiIdleTimer.Stop();
                        this.antiIdleTimer.Start();
                    }
                    ));
                }
                else
                {
                    this.antiIdleTimer.Stop();
                    this.antiIdleTimer.Start();
                }
            }
        }

        public void InvalidateCell(int row, int col)
        {
            InvalidateRect(row, col, row, col);
        }

        public void InvalidateRect(int top, int left, int bottom, int right)
        {
            int l = (left - 1) * this.charWidth;
            int t = (top - 1) * this.charHeight;
            int w = this.charHeight * (right - left + 1);
            int h = this.charHeight * (bottom - top + 1);
            Invalidate(new Rectangle(l, t, w, h));
        }
        #endregion

        void CloseForm()
        {
            Form form = (Form)this.FindForm();
            if (form != null)
            {
                if (InvokeRequired)
                {
                    Invoke(new MethodInvoker(form.Close));
                }
                else
                {
                    form.Close();
                }
            }
        }
    }
}
